package club.jdiy.dev.controller;

import club.jdiy.admin.interceptor.GuestDisabled;
import club.jdiy.core.base.domain.DictInfo;
import club.jdiy.core.base.domain.Pager;
import club.jdiy.core.base.domain.Ret;
import club.jdiy.core.ex.JDiyException;
import club.jdiy.core.sql.*;
import club.jdiy.dev.entity.JDiyUi;
import club.jdiy.dev.helper.OptsHelper;
import club.jdiy.dev.meta.*;
import club.jdiy.dev.service.JDiyUiService;
import club.jdiy.dev.types.*;
import club.jdiy.dev.view.*;
import club.jdiy.utils.JsonUtils;
import club.jdiy.utils.StringUtils;
import club.jdiy.utils.UrlUtils;
import club.jdiy.utils.poi.ExcelUtils;
import club.jdiy.utils.poi.MetaSet;
import freemarker.template.TemplateException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

@Controller
@RequestMapping("mgmt/JDiy")
@Slf4j
public class JDiyLsController extends JDiyUiCtrl<JDiyUi, JDiyUiService> implements JDiyController {
    private static class WherePart {
        private final String table;
        private final String _where;
        private final String _orderBy;
        private final Set<String> joins;

        public WherePart(String table, String _where, String orderBy, Set<String> joins) {
            this.table = table;
            this._where = _where;
            this._orderBy = orderBy;
            this.joins = joins;
        }

        public Args getArgs() {
            StringBuilder tb = new StringBuilder(table + " o");
            for (String s : joins) tb.append(" ").append(s);
            return new Args(tb.toString(), _where + _orderBy, "o.*");
        }

        public Args getSumArgs(String sunFields) {
            StringBuilder tb = new StringBuilder(table + " o");
            for (String s : joins) tb.append(" ").append(s);
            return new Args(tb.toString(), _where, sunFields);
        }
    }

    @GuestDisabled
    @RequestMapping("list.ajax.{uid}.{btnId}")
    @ResponseBody
    public Ret<?> do_ajax(@PathVariable String uid, @PathVariable String btnId, String[] id) {
        return super.do_ajax("list", uid, btnId, id);
    }


    @RequestMapping("{_referrerMenuId}/list.{opType}.{uid}")
    public String list(@PathVariable String uid,
                       @PathVariable String opType, //打开类型：dialog | tab |  lookup
                       @RequestParam Map<String, String> qo,
                       @RequestParam(defaultValue = "false") Boolean _lookupMulti, //opType=lookup时指定是否多选
                       @RequestParam(defaultValue = "name") String _lookupField, //opType=lookup时指定显示字段。默认为name
                       @PathVariable String _referrerMenuId,//来源主菜单(for权限控制)
                       //String _addFilter,//附加条件，如lookup控件等供调用方添加
                       String orderField, String orderDirection,
                       Integer pageSize, @RequestParam(defaultValue = "1") int page,
                       HttpServletResponse response,
                       ModelMap map) throws Exception {

        ListUiMeta uiMeta;
        try {
            uiMeta = service.getUiMeta(uid);
        } catch (Exception ex) {
            ex.printStackTrace();
            return Ret.direct(response, Ret.Type.msg, "<div style=\"margin:10px;color:red;\">目标界面配置有误，请跟踪控制台错误日志并检查！</div>");
        }
        FtlParser parser = createFtlParser();

        parser.addVariable("qo", qo);
        ListHandler handler = handlers.getHandler(uiMeta.getPageHandler(), ListHandler.class).orElse(new DefaultListHandler());

        preUiMeta(uiMeta, parser);

        Map<String, Map<String, DictInfo>> kvMaps = new HashMap<>();
        WherePart wherePart = __preQuery(uiMeta, parser, qo, handler, orderField, orderDirection, kvMaps, map);
        Args args = wherePart.getArgs();
        if (pageSize == null || pageSize < 1) {
            pageSize = uiMeta.getPageSize() < 1 ? 15 : uiMeta.getPageSize();
        }
        args.setPageSize(pageSize).setAbsPage(page);
        Dao dao = context.getDao();
        Pager<Rs> rsPager = handler.onView(dao.ls(args), Collections.unmodifiableMap(qo));//要先onView之后再做预处理，不然自定义模板不能生效．
        //buttons预处理：
        boolean hasBat = false;
        List<ButtonMeta> topBtns = new ArrayList<>();
        parser.addVariable("qo", qo);//不要移除（上面preQuery被改了，这儿重新设入）
        for (ButtonMeta it : uiMeta.getTopButtons()) {
            if ((!StringUtils.hasText(it.getConditionShow()) || JDiyLsController.ifCondition(it, uiMeta, null, parser))
                    && (!StringUtils.hasText(it.getGrantCode()) || context.hasGrant(_referrerMenuId + ":" + it.getGrantCode()))) {
                it.setTitle(parser.parse(it.getTitle()));
                it.setPageParam(parser.parse(it.getPageParam()));
                if (BtnActTpl.link == it.getAct() || BtnActTpl.post == it.getAct())
                    it.setOutLink(parser.parse(it.getOutLink()));
                else if (BtnActTpl.ajax == it.getAct()) {
                    it.setAjaxUrl(parser.parse(it.getAjaxUrl()));
                }
                if (handler.onButton(it)) {
                    topBtns.add(it);
                    hasBat = hasBat || BtnActTpl.del == it.getAct() || BtnActTpl.update == it.getAct() || BtnActTpl.exec == it.getAct() || BtnActTpl.ajax == it.getAct() || BtnActTpl.edit == it.getAct() || BtnActTpl.post == it.getAct();
                }
            }
        }
        List<ButtonMeta> rowButtonMeta = Arrays.stream(uiMeta.getRowButtons())
                .filter(it -> !StringUtils.hasText(it.getGrantCode()) || context.hasGrant(_referrerMenuId + ":" + it.getGrantCode()))
                .collect(Collectors.toList());
        if (rowButtonMeta.size() < 1) uiMeta.setMge(false);


        //输出预处理：
        Map<String, Rs> _ktbmap = new HashMap<>();
        List<List<ButtonMeta>> rowButtons = new ArrayList<>();
        for (Rs rs : rsPager.getItems()) {
            rowPacket(rs, uiMeta.getColumns(), parser, kvMaps, _ktbmap, false);
            parser.addVariable("vo", rs);
            rs.put("_id", rs.id());
            List<ButtonMeta> __btnList = new ArrayList<>();
            for (ButtonMeta it : rowButtonMeta) {
                ButtonMeta btn = it.clone();
                if (handler.onButton(btn, rs)) {
                    if (StringUtils.isEmpty(btn.getConditionShow()) || ifCondition(btn, uiMeta, rs, parser)) {
                        btn.setPageParam(parser.parse(it.getPageParam()));
                        switch (it.getAct()) {
                            case edit:
                            case view:
                                String idEqStr = rs.getPrimaryKey() + "=";
                                if (StringUtils.isBlank(btn.getPageParam())) btn.setPageParam(idEqStr + rs.id());
                                else if (!btn.getPageParam().startsWith(idEqStr) && !btn.getPageParam().contains("&" + idEqStr))
                                    btn.setPageParam(btn.getPageParam() + "&" + idEqStr + rs.id());
                                break;
                            case update:
                            case del:
                            case exec:
                                btn.setPageParam("id=" + rs.id());
                                break;
                            case page:
                                break;
                            case link:
                            case post:
                                btn.setOutLink(parser.parse(it.getOutLink()));
                                break;
                            case ajax:
                                btn.setAjaxUrl(new UrlUtils(parser.parse(it.getAjaxUrl())).set("id", rs.id()).toString());
                                break;
                        }
                        btn.setTitle(parser.parse(it.getTitle()));
                        __btnList.add(btn);
                    }
                }
            }
            rowButtons.add(__btnList);
        }

        if ("lookup".equals(opType)) {//查找带回部分功能不在页面显示
            uiMeta.setExcel(false);
            uiMeta.setMge(false);
            hasBat = false;
            topBtns = null;
        }

        //================================底部合计
        if (rsPager.getRowCount() > 0) {
            Set<String> sums = new HashSet<>();
            for (ColumnMeta cm : uiMeta.getColumns()) {
                if (cm.isSum()) sums.add("sum(o." + cm.getField() + ") as " + cm.getField());
            }
            if (!sums.isEmpty()) {
                Rs sumRs = dao.rs(wherePart.getSumArgs(StringUtils.join(sums, ",")));
                if (!sumRs.isNew()) map.put("sumData", sumRs);
            }
        }

        map.put("uiMeta", uiMeta);
        map.put("opType", opType);

        map.put("topBtns", topBtns);
        map.put("hasBat", hasBat);
        map.put("qo", qo);
        map.put("rowButtons", rowButtons);
        map.put("pager", rsPager);
        map.put("orderField", orderField);
        map.put("orderDirection", orderDirection);
        map.put("_lookupMulti", _lookupMulti);
        map.put("_lookupField", _lookupField);
        map.put("_referrerMenuId", _referrerMenuId);
        return "list.render";
    }


    @ResponseBody
    @RequestMapping("list.cascade.{uid}")
    public Ret<?> cascadeOptions(@PathVariable String uid, String name, String value) {
        try {
            ListUiMeta uiMeta;
            try {
                uiMeta = service.getUiMeta(uid);
            } catch (Exception ex) {
                return Ret.error(ex);
            }

            Map<String, String> qo = new HashMap<>();
            qo.put(name, value);

            FtlParser parser = createFtlParser();
            parser.addVariable("qo", qo);

            Map<String, Collection<OptsHelper.Entry>> voOptions = Arrays.stream(uiMeta.getQos())
                    .filter(input ->
                            input.isCascade()
                                    && (input.getType() == QoTpl.select)
                                    && JsonUtils.stringify(input).contains("${qo." + name)
                    )
                    .collect(Collectors.toMap(QoMeta::getId, input -> optsHelper.listOptions(input, parser), (a, b) -> b));
            return Ret.success(voOptions);
        } catch (Exception e) {
            e.printStackTrace();
            return Ret.error(e);
        }
    }

    @RequestMapping("list.export.{uid}")
    public void export(@PathVariable String uid, @RequestParam Map<String, String> qo, String orderField, String orderDirection,
                       HttpServletRequest request, HttpServletResponse response, ModelMap map) throws Exception {

        ListUiMeta uiMeta;
        try {
            uiMeta = service.getUiMeta(uid);
        } catch (Exception ex) {
            ex.printStackTrace();
            response.getWriter().println("目标界面配置有误，请跟踪控制台错误日志并检查！");
            return;
        }

        FtlParser parser = createFtlParser();
        parser.addVariable("qo", qo);
        ListHandler handler = handlers.getHandler(uiMeta.getPageHandler(), ListHandler.class).orElse(new DefaultListHandler());

        Map<String, Map<String, DictInfo>> kvMaps = new HashMap<>();
        WherePart wherePart = __preQuery(uiMeta, parser, qo, handler, orderField, orderDirection, kvMaps, map);

        int flushCache = 300;
        ExcelUtils et = new ExcelUtils(flushCache);
        MetaSet[] _customExport = handler.customExport();
        MetaSet[] ms = _customExport != null && _customExport.length > 0 ? _customExport : Arrays.stream(uiMeta.getColumns())
                .filter(col -> col.getFormat() != ColFormatTpl.img && col.getFormat() != ColFormatTpl.file)
                .map(col -> new MetaSet(col.getLabel(), col.getWidth() < 50 ? 200 : col.getWidth(), col.getId()))
                .toArray(MetaSet[]::new);
        et.addSheet("导出数据", ms);
        String xlsFileName = parser.parse(uiMeta.getExcelName());
        if (StringUtils.isEmpty(xlsFileName)) xlsFileName = "查询结果导出.xlsx";
        else if (!xlsFileName.endsWith(".xls") && !xlsFileName.endsWith(".xlsx")) xlsFileName += ".xlsx";

        Dao dao = context.getDao();
        Args args = wherePart.getArgs();
        args.setPageSize(flushCache).setAbsPage(1);
        Pager<Rs> pager = handler.onView(dao.ls(args), Collections.unmodifiableMap(qo));//要先onView之后再做预处理，不然自定义模板不能生效．
        while (pager != null && pager.getPage() <= pager.getPageCount()) {
            List<Rs> items;
            Map<String, Rs> _ktbmap = new HashMap<>();
            items = pager.getItems().stream().map(rs ->
                            rowPacket(rs, uiMeta.getColumns(), parser, kvMaps, _ktbmap, true))
                    .collect(Collectors.toList());
            et.addData(items);
            if (pager.getPage() < pager.getPageCount()) {
                args.setAbsPage(pager.getPage() + 1);
                pager = dao.ls(args);
            } else pager = null;
        }
        et.output(request, response, xlsFileName);
    }

    private WherePart __preQuery(ListUiMeta uiMeta, FtlParser parser, Map<String, String> qo, ListHandler handler, String orderField, String orderDirection,
                                 Map<String, Map<String, DictInfo>> kvMaps,
                                 ModelMap map) {
        parser.addVariable("qo", qo);

        Set<String> joinSet = new HashSet<>();
        ListWhere _where = new ListWhere();
        if (StringUtils.isNotBlank(uiMeta.getSqlFilter())) {
            try {
                String s1 = parser.parse(uiMeta.getSqlFilter());
                //fix:两边加括号防止用户输入的附加条件内容中有or子句导致其它查询条件无效
                if (StringUtils.hasText(s1)) _where.and("(" + s1 + ")");
            } catch (Exception ex) {
                ex.printStackTrace();
                throw new JDiyException("附加查询条件(" + uiMeta.getSqlFilter() + ")配置有误，请检查。");
            }
        }
        if (StringUtils.isNotBlank(qo.get("_addFilter"))) {
            try {
                String s1 = new String(Base64.getDecoder().decode(qo.get("_addFilter")));
                //fix:两边加括号防止用户输入的附加条件内容中有or子句导致其它查询条件无效
                if (StringUtils.hasText(s1)) _where.and("(" + s1 + ")");
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        List<QoMeta> qoList = new ArrayList<>();
        if (uiMeta.getQos() != null) Collections.addAll(qoList, uiMeta.getQos());
        handler.beforeQuery(qoList, _where);


        Map<String, Collection<OptsHelper.Entry>> qoOptions = new HashMap<>();
        TableInfo tableInfo = context.getDao().getTableInfo(uiMeta.getMainTable());
        boolean hasCascade = false;
        Map<String, String> qoForCascade = new HashMap<>();

        for (QoMeta it : qoList) {
            if (QoTpl.select == it.getType() || QoTpl.treeSelect == it.getType()) {
                qoForCascade.put(it.getField(), qo.get(it.getId()));
                if (it.isCascade()) hasCascade = true;
            }

            String q = qo.get(it.getId());
            String initVal;
            try {
                initVal = parser.parse(it.getInitial());
            } catch (Exception e) {
                initVal = it.getInitial();
            }
            if(StringUtils.hasText(initVal)) {
                if (StringUtils.isEmpty(q)) {
                    qo.put(it.getId(), q = initVal);//设置默认条件
                }
            }

            if (StringUtils.isEmpty(q) ||
                    !handler.onQuery(it, q, _where) ||
                    (it.getJoinType() == null || it.getJoinType() == QoJoinTpl.manyToOne) && !tableInfo.getColumns().containsKey(it.getField()))
                continue;

            if (it.getJoinType() == QoJoinTpl.sql) {
                parser.addVariable("value", q);
                try {
                    String s1 = parser.parse(it.getSql());
                    //fix:两边加括号防止用户输入的附加条件内容中有or子句导致其它查询条件无效
                    if (StringUtils.hasText(s1)) _where.and("(" + s1 + ")");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                parser.removeVariable("value");
                continue;
            }

            StringBuilder some = new StringBuilder();
            String expl = "o." + it.getField(), sb = "";
            if (it.getJoinType() != null) {
                TableInfo joinTableInfo = context.getDao().getTableInfo(it.getJoinTable());
                switch (it.getJoinType()) {
                    case manyToOne:
                        expl = it.getJoinTable() + "." + it.getJoinField();
                        joinSet.add("left join " + it.getJoinTable() + " on " + it.getJoinTable() +
                                "." + joinTableInfo.getPrimaryKey() + "=o." + it.getField());
                        break;
                    case oneToMany:
                        //todo exists 合并
                        expl = "t." + it.getJoinField();
                        some = new StringBuilder(" exists (select 0 from " + it.getJoinTable() + " t where t." +
                                it.getJoinRef() + "=o." + tableInfo.getPrimaryKey() + " and ");
                        sb = ")";
                        break;
                    case manyToMany:
                        //todo exists 合并
                        expl = "t." + it.getJoinField();
                        some = new StringBuilder(" exists (select 0 from " + it.getJoinTable() + " t," + it.getManyTable() +
                                " r where r." + it.getManyField0() + "=o." + tableInfo.getPrimaryKey() +
                                " and r." + it.getManyField1() + "=t." + joinTableInfo.getPrimaryKey() + " and ");
                        sb = ")";
                        break;
                }
            }
            q = q.trim().replaceAll("'", "''");
            if (it.getOper() == OperTpl.like || it.getOper() == OperTpl.not_like) {
                some.append(expl).append(" ").append(it.getOper().getValue()).append(" '%")
                        .append(q.trim().replaceAll("'", "''")).append("%'");
            } else if (QoTpl.integer == it.getType() || QoTpl.number == it.getType()) {
                try {
                    Number v = QoTpl.number == it.getType() ? Double.parseDouble(q) : Integer.parseInt(q);
                    some.append(expl).append(it.getOper().getValue()).append(v);
                } catch (Exception ignore) {
                    qo.remove(it.getId());
                }
            } else if (QoTpl.treeSelect == it.getType()) {
                if (OperTpl.treeAll == it.getOper()) {
                    TreeUiMeta treeMeta = service.getUiMeta(it.getTreeId());
                    TableInfo treeTableInfo = context.getDao().getTableInfo(treeMeta.getMainTable());
                    some.append(" exists (select 0 from ").append(treeTableInfo.getTableName())
                            .append(" _t where _t.").append(treeTableInfo.getPrimaryKey()).append("=")
                            .append(expl).append(" and _t.")
                            .append(treeMeta.getPathField()).append(" LIKE '%.").append(q).append(".%')");
                } else {
                    some.append(expl).append("='").append(q).append("'");
                }
            } else {
                if (it.getType() == QoTpl.date && it.getOper() == OperTpl.lte && !q.contains(" ")) q += " 23:59:59";
                some.append(expl).append(it.getOper().getValue()).append("'").append(q).append("'");
            }

            _where.and(some + sb);
        }


        parser.addVariable("qo", qoForCascade);
        for (QoMeta it : qoList) {
            if (QoTpl.select == it.getType()) {
                qoOptions.put(it.getId(), optsHelper.listOptions(it, parser));
            }
        }

        map.put("qoOptions", qoOptions);
        map.put("qoList", qoList);
        map.put("hasCascade", hasCascade);

        String realOrderField = Arrays.stream(uiMeta.getColumns())
                .filter(it -> it.getId().equals(orderField)).findFirst().map(ColumnMeta::getField).orElse("");
        kvCachedWrap(kvMaps, uiMeta.getColumns());

        //处理查询:
        String sort = uiMeta.getDefaultSort();
        if (StringUtils.hasText(realOrderField) && tableInfo.getColumns().containsKey(realOrderField)) {
            sort = realOrderField;
            if (orderDirection != null && !"".equals(orderDirection)) sort += " " + orderDirection;
        }
        if (sort != null) sort = sort.trim();

        return new WherePart(uiMeta.getMainTable(), _where.toString(),
                StringUtils.hasText(sort) ? " ORDER BY " + (sort.startsWith("o.") ? sort : "o." + sort) : ""
                , joinSet);
    }

    static boolean ifCondition(ButtonMeta btn, UiMeta uiMeta, Rs rs, FtlParser parser) {
        try {
            return parser.condition(btn.getConditionShow());
        } catch (Exception ex) {
            log.error("\r\n按钮的[显示条件]配置有误，执行条件抛出异常，请检查界面配置。\r\n界面ID:" + uiMeta.getId() + "　按钮ID:" + btn.getId() +
                    "　按钮标题:" + btn.getText() + "\r\n出错的按钮显示条件配置内容：" + btn.getConditionShow() +
                    (rs == null ? "" : "\r\n业务数据:" + uiMeta.getMainTable() + "(" + rs.getPrimaryKey() + "=" + rs.id() + ")"));
            ex.printStackTrace();
            return false;
        }
    }

    @Resource
    private OptsHelper optsHelper;
    @Resource
    private Handlers handlers;
}
